热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

临界|被覆_Linux下线程同步(带你了解什么是互斥锁死锁读写锁条件变量信号量等)

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Linux下线程同步(带你了解什么是互斥锁死锁读写锁条件变量信号量等)相关的知识,希望对你有一定的参考价值。线

篇首语:本文由编程笔记#小编为大家整理,主要介绍了Linux下线程同步(带你了解什么是互斥锁死锁读写锁条件变量信号量等)相关的知识,希望对你有一定的参考价值。



线程同步概念

假设有四个线程A、B、C、D,当前一个线程A对内存中的共享资源进行访问的时候,其他线程B、C、D都不可以对这块内存进行操作,直至A对这块内存访问完毕为止,B、C、D中的一个才能访问这块内存,剩余的俩个需要继续阻塞等待,一次类推,直到所有的线程都对这块内存操作完毕。线程对内存的这种访问方式称为线程同步,所谓的现成的同步并不是多个线程同时对内存访问,而是按照先后顺序依次进行的。

源码:

结果图:

CPU对于寄存器、一级缓存、二级缓存、三级缓存是独占的。用于存储处理的数据和线程的状态信息,数据被处理完需要再次被写入到物理内存中,物理内存数据也可以通过文件IO操作写入到磁盘中。

上述程序中,俩个线程共用全局变量number当线程变成运行态之后开始计数时,从物理内存(Memory)中加载数据,然后让数据放到CPU进行运算,最后将结果跟新到物理内存中。

如果线程A执行这个过程期间就失去了CPU时间片,线程A被挂起了最新的数据没能更新到物理内存中(保存到了寄存器中),线程B变成了运行态之后从物理内存读取数据,这个时候B拿的数据就不是最新数据。只能基于旧的数据往后计数,然后失去时间片。线程A在得到时间片变成运行态时,第一件事就是将上次没更新到内存的数据更新到内存(从寄存器拿上次),但是这样会导致B线程已经跟新到内存的数据被覆盖掉。活白干了。最终导致有些数据会被重复很多次。


同步方式

对于多个线程访问共享资源出现数据混乱的问题,需要进行线程同步。常用的线程同步方式有四种:互斥锁、读写锁、条件变量、信号量。所谓的共享资源就是多个线程共同访问的变量,这些变量通常为全局数据区变量或者堆区变量,这些变量对于的共享资源也被称之为临界资源。


互斥锁

互斥锁函数
互斥锁是线程同步最常用的一种方式,通过互斥锁可以锁定一个代码块,被锁定的这个代码块,所有线程只能顺序执行(不能并行处理),这样多线程访问共享资源数据混乱的问题就可以被解决了,需要付出的代价就是执行效率的降低,因为默认临界区多个线程可以并行处理的,现在只能串性处理。

在Linux中互斥锁的类型为pthread_mutex_t,创建一个这样类型的变量就得到了一把互斥锁。ptherad_mutex_t mutex;

在创建的锁对象中保存了当前这把锁的状态信息:锁定还是打开,如果是锁定状态还记录了给这把锁加锁的线程信息(线程ID)。一个互斥锁变量只能被一个线程锁定,被锁定之后其他线程再对互斥锁变量加锁就会被阻塞,直到这把互斥锁被解锁,被阻塞的线程才能被解除阻塞,一般情况下,每一个共享资源对应一把互斥锁,锁的个数与线程的个数无关

初始互斥锁: int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);

释放互斥锁: int pthread_mutex_destroy(pthread_mutex_t *mutex);

restrict:关键字,用来修饰指针,只有这个关键字修饰的指针可以访问指向的内存地址。其他指针不行
mutex:互斥锁变量地址
attr:互斥锁的属性,一般使用默认属性就可以,这个参数指定NULL

修改互斥锁状态
int pthread_mutex_lock(pthread_mutex *mutex);
该函数被调用时,首先会判断参数mutex互斥锁中的状态是不是锁定状态。
• 若没有被锁定,时打开的。这个线程可以加锁成功。该锁会记录那个线程加锁成功了
• 锁被锁定了,其他线程加锁就会失败,这些线程就会阻塞到这把锁上。
• 当把这把锁解开之后,阻塞在这把锁的线程就解除阻塞了,并且这些线程通过竞争的方式对这把锁加锁,没抢到锁的线程继续阻塞。
例如:你要去上厕所,发现里面有人,你就在外边等着。

尝试加锁
int pthread_mutex_trylock(pthread_mutex_t *mutex);
• 如果这把锁没有被锁定,线程加锁成功
• 如果这把锁被锁定后了。调用这个函数加锁的线程,不会被阻塞,加锁失败直接返回错误码。
例如:你要去上厕所,发现里面有人,你就去干别的事情了,

互斥锁解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
那个线程加的锁,必须那个线程去解锁。


互斥锁的使用

需要先创建一个互斥锁

再使用互斥锁对需要加锁的位置进行加锁和解锁操作。

全部代码如下:

运行图如下:


死锁

当多个线程访问共享资源时,需要加锁,如果锁使用不当,就会造成死锁现象。如果线程死锁造成的后果是:所有线程都被阻塞,并且线程的阻塞是无法解开的(因为可以解锁的线程也被阻塞了)。

例如:加锁后忘记解锁

再例如:


  1. 当我们加锁后进入if语句后退出(return)了,等其他线程再进来就发现当前已被锁,就阻塞自己。
  2. 重复加锁(多加锁了一次)

多个线程对一个临界资源进行访问我们需要一把锁
一个线程对多个临界资源进行访问我们需要多把锁

如何避免死锁:
• 对共享资源访问完毕之后,一定要解锁。或者在加锁的时候使用tylock
• 如果程序中有多把锁,可以控制对锁的访问顺序(顺序访问共享资源,但是有些情况下是做不到的),另外也可以在对其他互斥锁操作前,先释放当前线程拥有的互斥锁。
• 项目程序中可以引入一些专门用于检测死锁的模块


读写锁

读写锁是一把锁,是互斥锁的升级版。在做读操作的时候可以提高程序执行效率,如果所有的线程都是做读操作,那么读是并行的,但是使用互斥锁,读操作也是串行的。

锁的类型为pthread_rwlock_t 这把锁既可以锁定读操作,还可以锁定写操作。
记录信息:


  1. 锁的状态:锁定/打开
  2. 锁定的是什么操作:读操作/写操作,使用了读写锁锁定了读操作,需要先解锁,再去锁定写操作。
  3. 哪个线程将这把锁锁上的

特点:
• 使用了读写锁的读锁锁定了临界区,线程对临界区的访问是并行的。读锁是共享的。
• 使用了读写锁的写锁锁定了临界区,线程对临界区的访问时串性的。写锁是独占的。
• 使用了读写锁分别对俩个临界区区加了读锁和写锁,俩个线程要同时访问着俩个临界区,访问写锁临界区的线程继续运行,访问读锁临界区的线程阻塞,因为写锁比读锁的优先级高。

如果说程序中所有的线程都对共享资源做写操作,使用读写锁没有优势。但是说程序中所有的线程对共享资源既有读也有写操作,使用读写锁更有优势。

初始化读写锁
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlock_t *restrict attr);

rwlock:读写锁地址,传出参数
attr:读写锁属性,一般使用默认属性,指定为NULL

释放读写锁占用的系统资源
int pthread_rwlock_destroy(pthread_rwlock_t *restrict rwlock);

rwlock:读写锁地址,传出参数

加锁读操作
int pthread_rwlock_rdlock(pthread_rwlock_t * );
如果读写锁是打开的,那么加锁成功,
如果读写锁锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的。
如果读写锁锁定了写操作,调用这个函数会阻塞。

如果加读锁失败,不会阻塞当前线程,直接返回错误号 有效避免死锁
int pthread_rwlock_tryrdlock(pthread_rwlock_t * rwlock);
如果读写锁是打开的,那么加锁成功,
如果读写锁锁定了读操作,调用这个函数依然可以加锁成功,因为读锁是共享的。
如果读写锁锁定了写操作,不会阻塞当前线程,函数会返回一个错误码。

加锁写操作
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
如果读写锁是打开的,那么加锁成功,
如果读写锁锁定了读操作,调用这个函数会阻塞。
如果读写锁锁定了写操作,调用这个函数会阻塞。

如果加锁失败,不会阻塞当前线程,直接返回错误号 有效避免死锁
int pthread_rwlock_trywrdlock(pthread_rwlock_t * rwlock);
如果读写锁是打开的,那么加锁成功,
如果读写锁锁定了读操作,不会阻塞当前线程,函数会返回一个错误码。
如果读写锁锁定了写操作,不会阻塞当前线程,函数会返回一个错误码。

解锁 读锁写锁都可以
int pthread_rwlock_unlock(pthread_rwlock_t * rwlock);


读写锁使用

题目要求:8个线程操作同一个全局变量,3个线程不定时写同一全局资源,5个线程不定时读同一线程。
源码如下:

运行效果图:


条件变量

条件变量函数
条件变量的主要作用不是处理线程同步,而是进行线程阻塞。如果多线程程序中只使用条件变量无法实现线程的同步,必须要配合互斥锁来使用。虽然条件变量和互斥锁都能阻塞线程,但是二者的效果是不一样得到,二者的区别如下:
• 假设有A-Z26个线程,这26个线程共同访问同一把互斥锁,如果A线程加锁成功,那么其余B-Z线程访问互斥锁都阻塞,所有的线程只能顺序访问临界区。
• 条件变量只有在满足指定条件下才会阻塞线程,如果条件不满足,多个线程可以同时进入临界区,同时读写临界区资源,这种情况下还是会出现共享资源中数据混乱。

一般情况下条件变量用于处理生产者和消费者模型,并且和互斥锁配合使用,条件变量类型对应的类型为pthread_cond_t,

被条件变量阻塞的线程的信息会被记录到这个变量中,以便在解除阻塞的时候使用。

初始化
int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
cond:条件变量的地址
attr:条件变量属性,一般使用默认属性,指定为NULL

销毁释放资源
int pthread_cond_destroy(pthread_cond_t *cond);
cond:条件变量的地址

阻塞函数
int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex *restrict mutex);
该函数在阻塞线程的时候,需要一个互斥锁参数,这个互斥锁主要功能是进行线程同步,让线程顺序进入临界区,避免出现数共享资源的数据混乱。会对互斥锁做俩件事
• 1、在阻塞线程时候,如果线程已经对互斥锁mutex上锁,那么会将这把锁打开,这样避免死锁。
• 2、当线程解除阻塞的时候,函数内部会帮助这个线程再次将这个mutex互斥锁锁上,继续向下访问临界区。

阻塞时长函数
将线程阻塞一定时长,时间到了,线程就解除阻塞。

struct timespec
time_t tv_sec;
time_t tv_nsec;
;

int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
前俩个参数和前面阻塞哈数一样,第三个参数是阻塞时长,

唤醒阻塞函数(至少唤醒一个)
int pthread_cond_signal(pthread_cond_t *cond);

唤醒阻塞函数(全部唤醒)
int pthread_cond_broadcast(pthread_cond_t *cond);


生产者和消费者

场景描述:使用条件变量实现生产者和消费者模型,生产者有5个,往链表头部添加节点,消费者也有5个,删除链表尾巴的节点

源码如下:

接下里我们该做线程同步问题
• 生产者和消费者区域全部上锁
• 当生产者发现生产数量超过5时,阻塞生产者,唤醒消费者。当生产者生产完毕,也需要唤醒消费者。
• 当消费者发现没有产品时,阻塞消费者,唤醒生产者。当消费者消费完毕,也需要唤醒生产者


信号量

信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作。信号量不一定是锁定某一个资源,而是流程上的概念,比如:有 A,B 两个线程,B 线程要等 A 线程完成某一任务以后再进行自己下面的步骤,这个任务并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。流水线

信号量(信号灯)与互斥锁和条件变量的主要不同在于” 灯” 的概念,灯亮则意味着资源可用,灯灭则意味着不可用。信号量主要阻塞线程,不能完全保证线程安全,如果要保证线程安全,需要信号量和互斥锁一起使用。

信号量和条件变量一样用于处理生产者和消费者模型,用于阻塞生产者线程或者消费者线程的运行。
信号的类型为 sem_t 对应的头文件为

初始化信号量/信号灯
int sem_init(sem_t *sem, int pshared, unsigned int value);
sem:信号量变量地址
pshared:0线程同步,非0进程同步
value:初始化当前信号量拥有的资源数(>=0),如果资源数位0,线程就会被阻塞了。

资源释放,线程销毁之后调用这个函数
int sem_destroy(sem_t *sem);

消耗函数
int sem_wait(sem_t *sem); 函数调用时sem中的资源就会被消耗一个,资源数-1
当线程调用这个函数,并且sem>0,线程不会被阻塞,线程会占用sem中的一个资源,因此资源数-1,直到sem中的资源数减为0时。资源被耗尽,因此线程也就被阻塞了。

int sem_trywait(sem_t *sem); 函数调用时sem中的资源就会被消耗一个,资源数-1
当线程调用这个函数,并且sem>0,线程不会被阻塞,线程会占用sem中的一个资源,因此资源数-1,直到sem中的资源数减为0时。资源被耗尽,但是线程不会被阻塞,直接返回错误号,因此可以在程序中添加判断分支,用于处理获取资源失败之后的情况。

解除阻塞时间函数
// 表示的时间是从1971.1.1到某个时间点的时间, 总长度使用秒/纳秒表示

struct timespec
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds [0 .. 999999999] */
;

// 调用该函数线程获取sem中的一个资源,当资源数为0时,线程阻塞,在阻塞abs_timeout对应的时长之后,解除阻塞。
// abs_timeout: 阻塞的时间长度, 单位是s, 是从1970.1.1开始计算的
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

该函数的参数 abs_timeout 和 pthread_cond_timedwait 的最后一个参数是一样的,使用方法不再过多赘述。当线程调用这个函数,并且 sem 中的资源数 >0,线程不会阻塞,线程会占用 sem 中的一个资源,因此资源数 - 1,直到 sem 中的资源数减为 0 时,资源被耗尽,线程被阻塞,当阻塞指定的时长之后,线程解除阻塞。

增加资源函数
// 调用该函数给sem中的资源数+1
int sem_post(sem_t *sem);
调用该函数会将 sem 中的资源数 +1,如果有线程在调用 sem_wait、sem_trywait、sem_timedwait 时因为 sem 中的资源数为 0 被阻塞了,这时这些线程会解除阻塞,获取到资源之后继续向下运行。

查看信号量函数
// 查看信号量 sem 中的整形数的当前值, 这个值会被写入到sval指针对应的内存中
// sval是一个传出参数
int sem_getvalue(sem_t *sem, int *sval);


生产者消费者(一个资源量)

一个资源量就不需要线程同步。必然是单线程 , 只有资源总数为1时候,才可以不加锁
源代码:


生产者消费者(多个资源)

需要加锁,生产者和消费者共用一个链表,所以用一把锁。
锁要在信号量里面。不然有可能会造成死锁(比如:现在资源被消费完毕。然后一直被消费者抢到,进入阻塞,当全部线程被阻塞在消费者这边时,就造成了死锁)

源代码:


推荐阅读
  • 本文讨论了clone的fork与pthread_create创建线程的不同之处。进程是一个指令执行流及其执行环境,其执行环境是一个系统资源的集合。在调用系统调用fork创建一个进程时,子进程只是完全复制父进程的资源,这样得到的子进程独立于父进程,具有良好的并发性。但是二者之间的通讯需要通过专门的通讯机制,另外通过fork创建子进程系统开销很大。因此,在某些情况下,使用clone或pthread_create创建线程可能更加高效。 ... [详细]
  • 本文介绍了深入浅出Linux设备驱动编程的重要性,以及两种加载和删除Linux内核模块的方法。通过一个内核模块的例子,展示了模块的编译和加载过程,并讨论了模块对内核大小的控制。深入理解Linux设备驱动编程对于开发者来说非常重要。 ... [详细]
  • 在Docker中,将主机目录挂载到容器中作为volume使用时,常常会遇到文件权限问题。这是因为容器内外的UID不同所导致的。本文介绍了解决这个问题的方法,包括使用gosu和suexec工具以及在Dockerfile中配置volume的权限。通过这些方法,可以避免在使用Docker时出现无写权限的情况。 ... [详细]
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • Linux重启网络命令实例及关机和重启示例教程
    本文介绍了Linux系统中重启网络命令的实例,以及使用不同方式关机和重启系统的示例教程。包括使用图形界面和控制台访问系统的方法,以及使用shutdown命令进行系统关机和重启的句法和用法。 ... [详细]
  • Java序列化对象传给PHP的方法及原理解析
    本文介绍了Java序列化对象传给PHP的方法及原理,包括Java对象传递的方式、序列化的方式、PHP中的序列化用法介绍、Java是否能反序列化PHP的数据、Java序列化的原理以及解决Java序列化中的问题。同时还解释了序列化的概念和作用,以及代码执行序列化所需要的权限。最后指出,序列化会将对象实例的所有字段都进行序列化,使得数据能够被表示为实例的序列化数据,但只有能够解释该格式的代码才能够确定数据的内容。 ... [详细]
  • 本文详细介绍了Linux中进程控制块PCBtask_struct结构体的结构和作用,包括进程状态、进程号、待处理信号、进程地址空间、调度标志、锁深度、基本时间片、调度策略以及内存管理信息等方面的内容。阅读本文可以更加深入地了解Linux进程管理的原理和机制。 ... [详细]
  • 图解redis的持久化存储机制RDB和AOF的原理和优缺点
    本文通过图解的方式介绍了redis的持久化存储机制RDB和AOF的原理和优缺点。RDB是将redis内存中的数据保存为快照文件,恢复速度较快但不支持拉链式快照。AOF是将操作日志保存到磁盘,实时存储数据但恢复速度较慢。文章详细分析了两种机制的优缺点,帮助读者更好地理解redis的持久化存储策略。 ... [详细]
  • Linux环境变量函数getenv、putenv、setenv和unsetenv详解
    本文详细解释了Linux中的环境变量函数getenv、putenv、setenv和unsetenv的用法和功能。通过使用这些函数,可以获取、设置和删除环境变量的值。同时给出了相应的函数原型、参数说明和返回值。通过示例代码演示了如何使用getenv函数获取环境变量的值,并打印出来。 ... [详细]
  • 预备知识可参考我整理的博客Windows编程之线程:https:www.cnblogs.comZhuSenlinp16662075.htmlWindows编程之线程同步:https ... [详细]
  • 服务器上的操作系统有哪些,如何选择适合的操作系统?
    本文介绍了服务器上常见的操作系统,包括系统盘镜像、数据盘镜像和整机镜像的数量。同时,还介绍了共享镜像的限制和使用方法。此外,还提供了关于华为云服务的帮助中心,其中包括产品简介、价格说明、购买指南、用户指南、API参考、最佳实践、常见问题和视频帮助等技术文档。对于裸金属服务器的远程登录,本文介绍了使用密钥对登录的方法,并提供了部分操作系统配置示例。最后,还提到了SUSE云耀云服务器的特点和快速搭建方法。 ... [详细]
  • 海马s5近光灯能否直接更换为H7?
    本文主要介绍了海马s5车型的近光灯是否可以直接更换为H7灯泡,并提供了完整的教程下载地址。此外,还详细讲解了DSP功能函数中的数据拷贝、数据填充和浮点数转换为定点数的相关内容。 ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • 本文介绍了操作系统的定义和功能,包括操作系统的本质、用户界面以及系统调用的分类。同时还介绍了进程和线程的区别,包括进程和线程的定义和作用。 ... [详细]
author-avatar
SHW_keytae
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有